#!/usr/bin/python
#
# Copyright (C) 2012 Paul Wouters <pwouters@redhat.com>
#
# Based on old perl and shell code:
# Copyright (C) 2003 Sam Sgro <sam@freeswan.org> 
# Copyright (C) 2005-2008 Michael Richardson <mcr@xelerance.com>
# Copyright (C) 2005-2009 Paul Wouters <paul@xelerance.com>
#
# Based on "verify" from the FreeS/WAN distribution, (C) 2001 Michael 
# Richardson <mcr@freeswan.org>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.

import os, sys, subprocess, glob, socket


retcode = 0

conffile = "@FINALCONFFILE@"
confdir = "@IPSEC_DIR@"
ipsecbin = "@IPSEC_SBINDIR@/ipsec"

if not os.path.isfile(ipsecbin):
	# hopefully somewhere in our path then
	ipsecbin = "ipsec"

if not os.path.isfile(conffile):
	if not os.path.isfile("%s/ipsec.conf"%confdir):
		# try some fall backs
		if os.path.isfile("/etc/ipsec.conf"):
			print "WARNING: ipsec.conf not found at compiled-in location '%s/ipsec.conf', using /etc/ipsec.conf instead"%confdir
			confdir = "/etc/"
		elif os.path.isfile("/usr/local/etc/ipsec.conf"):
			print "WARNING: ipsec.conf not found at compiled-in location '%s/ipsec.conf', using /etc/ipsec.conf instead"%confdir
			confdir = "/usr/local/etc/"
		else:
			sys.exit("Failed to find ipsec.conf - checked %s, /etc and /usr/local/etc"%confdir)

# Should we print in colour by default?
colour = 0
try:
	p = subprocess.Popen("consoletype", stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
	output, err = p.communicate()
	output = output.strip()
	if output in ("vc","tty","pty"):
		colour = 1
except:
	try:
		p = subprocess.Popen("tput colors", stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
		output, err = p.communicate()
		if int(output) > 0:
			colour = 1
	except:
		pass


def printfun(text):
	# suppress newline
	sys.stdout.write("%-50s"%text)

def print_result(rcode, rtext):
	global colour
	global retcode
	OK = '\033[92m'
	WARN = '\033[93m'
	FAIL = '\033[91m'
	ENDC = '\033[0m'

	if rcode == "FAIL":
		retcode += 1
		if not rtext:
			rtext = "FAILED"
		if colour:
			print "\t[%s%s%s]"%(FAIL,rtext,ENDC)
		else:
			print "\t[%s]"%rtext
	elif rcode == "WARN":
		if not rtext:
			rtext = "WARNING"
		if colour:
			print "\t[%s%s%s]"%(WARN,rtext,ENDC)
		else:
			print "\t[%s]"%rtext
	elif rcode == "OK":
		if not rtext:
			rtext = "OK"
		if colour:
			print "\t[%s%s%s]"%(OK,rtext,ENDC)
		else:
			print "\t[%s]"%rtext
	else:
		print "INTERNAL ERROR - unknown rcode:%s"%rcode


def plutocheck():
	global retcode
        printfun("Checking that pluto is running")
	p = subprocess.Popen(["pidof", "pluto"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
	output, err = p.communicate()
	if not output:
		retcode += 1
		print_result("FAIL","FAILED")
		return
	else:
		print_result("OK","OK")

	# only if pluto is running, do the listen tests and ipsec  secrets test
	udp500check()
	tcp500check()
	udp4500check()
	tcp4500check()
	tcp10000check()
	ipsecsecretcheck()

# This is pretty broken, as you can enable forwarding specificaly via iptables as well
# It also won't find/exclude all kinds of interfaces. Also, lots of people have one
# ethernet and one wifi interface, but don't want to bridge these.
# So, mostly left here for historic reasons - candidate to be changed/removed
def forwardcheck():
	global retcode
	try:
		output = open("/proc/net/dev","r").read().strip()
	except:
		printfun("Checking for multiple interfaces")
		retcode += 1
		print_result("FAIL","UNEXPECTED KERNEL DEV LIST")
	count = 0
	for line in output.split("\n"):
		if ":" in line:
			if not "lo:" in line and not "ipsec" in line and not "mast" in line and not "virbr:" in line:
				# let's count this as a real physical interface
				count += 1
	if count > 1:
		printfun("Two or more interfaces found, checking IP forwarding")
		try:
			output = open("/proc/sys/net/ipv4/ip_forward","r").read().strip()
		except:
			print_result("FAIL","MISSING ip_forward proc file")
			retcode += 1
			return
		if output == "1":
			print_result("OK","OK")
		else:
			print_result("FAIL","FAILED")
			retcode += 1

def rpfiltercheck():
	global retcode
	fail = 0
	printfun("Checking rp_filter")
	for dirname in glob.glob("/proc/sys/net/ipv4/conf/*"):
		val = open("%s/rp_filter"%dirname,"r").read().strip()
		if val == "1":
			if fail == 0:
				print_result("FAIL","ENABLED")
			fail = 1
			printfun(" %s/rp_filter"%dirname)
			print_result("FAIL","ENABLED")
			retcode += 1
	if fail == 0:
			print_result("OK","OK")
	else:
		print "  rp_filter is not fully aware of IPsec and should be disabled"
		
# Check if any NAT or MASQUERADE would accidentally mess up our packets
def natcheck():
	global retcode
	printfun("Checking NAT and MASQUERADEing")
	print_result("WARN","TEST INCOMPLETE")

	# run iptables -t nat -L -n
	# grep for NAT or MASQ
	# for KLIPS
	#   grep over /proc/net/ipsec_eroute check for 'tun0x' then find overlap
	# for NETKEY
	# grep over ip xfrm state/policy and find overlap

def cmdchecks():
	global retcode
	printfun("Checking 'ip' command")
	if not os.path.isfile("/sbin/ip") and not os.path.isfile("/usr/sbin/ip"):
			print_result("FAIL","FAILED")
			retcode += 1
	p = subprocess.Popen(["ip", "xfrm"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
	output, err = p.communicate()
	if not "XFRM" in err:
			print_result("FAIL","IP XFRM BROKEN")
			retcode += 1
	else:
			print_result("OK","OK")

	printfun("Checking 'iptables' command")
	if not os.path.isfile("/sbin/iptables") and not os.path.isfile("/usr/sbin/iptables"):
			print_result("WARN","MISSING")
	else:
			print_result("OK","OK")

def udp500check():
	global retcode
	printfun(" Pluto listening for IKE on udp 500")
	try:
		p = subprocess.Popen(["ss", "-n", "-a", "-u", "sport = :500"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
		output, err = p.communicate()
		if ":500" in output:
			print_result("OK","OK")
		else:
			print_result("FAIL","FAILED")
			retcode += 1
	except:
			print_result("FAIL","FAILED")
			retcode += 1

# not yet implemented in *swan
def tcp500check():
	global retcode
	global sscmd
	printfun(" Pluto listening for IKE on tcp 500")
	if not sscmd:
			print_result("WARN","UNKNOWN")
			print "(install the 'ss' command to activate this test)"
			return
	try:
		p = subprocess.Popen([sscmd, "-n", "-l", "-t", "sport = :500"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
		output, err = p.communicate()
		if ":500" in output:
			print_result("OK","OK")
		else:
			print_result("WARN","NOT IMPLEMENTED")
	except:
			print_result("WARN","NOT IMPLEMENTED")

def udp4500check():
	global retcode
	global sscmd
	printfun(" Pluto listening for IKE/NAT-T on udp 4500")
	if not sscmd:
			print_result("WARN","UNKNOWN")
			print "(install the 'ss' command to activate this test)"
			return
	try:
		p = subprocess.Popen([sscmd, "-n", "-a", "-u", "sport = :4500"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
		output, err = p.communicate()
		if ":4500" in output:
			print_result("OK","OK")
		else:
			print_result("WARN","DISABLED")
	except:
			print_result("FAIL","DISABLED")
			retcode += 1

# not yet implemented in *swan
def tcp4500check():
	global retcode
	global sscmd
	printfun(" Pluto listening for IKE/NAT-T on tcp 4500")
	if not sscmd:
			print_result("WARN","UNKNOWN")
			print "(install the 'ss' command to activate this test)"
			return
	try:
		p = subprocess.Popen([sscmd, "-n", "-l", "-t", "sport = :4500"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
		output, err = p.communicate()
		if ":4500" in output:
			print_result("OK","OK")
		else:
			print_result("WARN","NOT IMPLEMENTED")
	except:
			print_result("WARN","NOT IMPLEMENTED")

# not yet implemented in *swan
def tcp10000check():
	global retcode
	global sscmd
	printfun(" Pluto listening for IKE on tcp 10000 (cisco)")
	if not sscmd:
			print_result("WARN","UNKNOWN")
			print "(install the 'ss' command to activate this test)"
			return
	try:
		p = subprocess.Popen([sscmd, "-n", "-l", "-t", "sport = :10000"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
		output, err = p.communicate()
		if ":10000" in output:
			print_result("OK","OK")
		else:
			print_result("WARN","NOT IMPLEMENTED")
	except:
			print_result("WARN","NOT IMPLEMENTED")

def installstartcheck():
	global retcode
	print "Verifying installed system and configuration files\n"
	printfun("Version check and ipsec on-path")
	try:
		p = subprocess.Popen(["ipsec", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
		output, err = p.communicate()
		if "swan" in output:
			print_result("OK","OK")
			print output.replace("Linux","").strip()
		else:
			print_result("FAIL","FAILED")
	except:
			print_result("FAIL","FAILED")

	printfun("Checking for IPsec support in kernel")
	if not os.path.isfile("/proc/net/ipsec_eroute") and not os.path.isfile("/proc/net/pfkey"):
		print_result("FAIL","FAILED")
		if "no kernel code presently loaded" in output:
			print("\n The ipsec service should be started before running 'ipsec verify'\n")
		return
	else:
		print_result("OK","OK")

	# we know we have some stack, so continue
	if os.path.isfile("/proc/net/ipsec_eroute"):
		installcheckklips()
	else:
		installchecknetkey()

def installcheckklips():
	global retcode
	# NAT-T patch check
	printfun(" KLIPS: checking for NAT Traversal support")
	try:
		natt = open("/sys/module/ipsec/parameters/natt_available","r").read().strip()
		if natt == "1":
			print_result("WARN","OLD STYLE")
		elif natt == "2":
			print_result("OK","OK")
		else:
			print_result("FAIL","UNKNOWN CODE")
	except:
		print_result("WARN","OLD/MISSING")

	# OCF patch check
	printfun(" KLIPS: checking for OCF crypto offload support ")
	try:
		ocf = open("/sys/module/ipsec/parameters/ocf_available","r").read().strip()
		if ocf == "1":
			print_result("OK","OK")
		elif ocf == "0":
			print_result("WARN","N/A")
		else:
			print_result("FAIL","UNKNOWN CODE")
	except:
		print_result("WARN","N/A")

	# SAref patch check
	saref = ""
	printfun(" KLIPS: IPsec SAref kernel support")
	try:
		saref = open("/proc/net/ipsec/saref","r").read().strip()
		if "refinfo patch applied" in saref:
			print_result("OK","OK")
		else:
			print_result("WARN","N/A")
			
	except:
		print_result("WARN","N/A")

	# SAref bind patch check
	printfun(" KLIPS: IPsec SAref Bind kernel support")
	if "bindref patch applied" in saref:
		print_result("OK","OK")
	else:
		print_result("WARN","N/A")
			

def installchecknetkey():
	global retcode
	print " NETKEY: Testing XFRM related proc values"
	for option in ( "send_redirects", "accept_redirects"):
		printfun("         ICMP default/%s"%option) 
		try:
			redir = open("/proc/sys/net/ipv4/conf/default/%s"%option,"r").read().strip()
		except:
			print_result("FAIL","VERY BROKEN KERNEL")
			return
		if redir == "0":
			print_result("OK","OK")
		else:
			print_result("FAIL","NOT DISABLED")
			print("\n  Disable /proc/sys/net/ipv4/conf/*/%s or NETKEY will cause act on or cause sending of bogus ICMP redirects!\n"%option)

	printfun("         XFRM larval drop")
	try:
		larval = open("/proc/sys/net/core/xfrm_larval_drop","r").read().strip()
	except:
		print_result("FAIL","OLD OR BROKEN KERNEL")
		return
	if larval == "1":
		print_result("OK","OK")
	else:
			print_result("FAIL","NOT ENABLED")

	
def randomdevcheck():
	global retcode
	printfun("Hardware random device")
	if os.path.isfile("/dev/hw_random") or os.path.isfile("/dev/hwrng"):
		p = subprocess.Popen(["pidof", "rngd"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
		output, err = p.communicate()
		if not output:
			p = subprocess.Popen(["pidof", "clrngd"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
			output, err = p.communicate()
			if not output:
				print_result("FAIL","NO RUNNING (cl)rngd DAEMON FOR HW")
			else:
				print_result("OK","CLRNGD")
		else:
			print_result("OK","RNGD")
	else:
			print_result("OK","N/A")

def ipsecsecretcheck():
	global retcode
	# we need to be root, because the only way to check is to reload them
	printfun(" Pluto ipsec.secret syntax")
	uid = os.getuid()
	if uid != 0:
		print_result("WARN","UNKNOWN")
		print " (run ipsec verify as root to test ipsec.secrets)"
		return

	p = subprocess.Popen(["ipsec","secrets"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
	output, err = p.communicate()
	if "ERROR" in output:
		print_result("FAIL","PARSE ERROR")
		for line in output.split("\n"):
			line = line.strip()
			if line and not "forgetting secrets" in line:
				print "  %s"%line
	else:
		print_result("OK","OK")

def ipsecconfcheck():
	global retcode
	printfun("Pluto ipsec.conf syntax")
	p = subprocess.Popen(["ipsec","addconn","--checkconfig"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
	output, err = p.communicate()
	if "syntax error" in err:
		print_result("FAIL","PARSE ERROR")
		print err
	else:
		print_result("OK","OK")

def configsetupcheck():
	global retcode
	p = subprocess.Popen(["ipsec","addconn","--configsetup"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
	configsetup, err = p.communicate()
	myid = ""
	oe = "no"
	# grab obsolete settings
	printfun("Checking for obsolete ipsec.conf options")
	if not err:
		print_result("OK","OK")
	else:
		print_result("WARN","OBSOLETE KEYWORD")
		err = err.replace("Warning"," Warning")
		print err[:-1]

	printfun("Opportunistic Encryption")
	for line in configsetup.split("\n"):
		# grab oe= setting
		if " oe=" in line:
			oe = line.split("oe='")[1].split("'")[0]
		if " myid=" in line:
			myid = line.split("myid='")[1].split("'")[0]
	if oe == "no":
		print_result("OK","DISABLED")
		return
	print_result("OK","ENABLED")

	if not myid:
		myid = socket.gethostname()

	printfun(" FQDN check (%s)"%myid)
	if "." in myid:
		print_result("OK","OK")
	else:
		print_result("FAIL","UNUSABLE HOSTNAME")

	printfun(" IPSECKEY DNS record present")
	if not os.path.isfile("/usr/bin/dig") and not os.path.isfile("/bin/dig"):
		print_result("WARN","UNKNOWN")
		print "(install the dig command to active this test)"
		return

	p = subprocess.Popen(["dig","+short","ipseckey", myid ], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
	ipseckey, err = p.communicate()
	if not ipseckey:
		print_result("FAIL","MISSING")
	try:
		short = ipseckey.split(" ")[4][:8]
		print_result("OK", "OK - %s"%short)
	except:
		print_result("FAIL","MANGLED")

def main():
	global retcode
	global sscmd
	if os.path.isfile("/usr/sbin/ss"):
		sscmd = "/usr/sbin/ss"
	elif os.path.isfile("/bin/ss"):
		sscmd = "/bin/ss"

	installstartcheck()
	ipsecconfcheck()
	randomdevcheck()
	forwardcheck()
	rpfiltercheck()
	plutocheck()
	natcheck()
	cmdchecks()
	configsetupcheck()
	if retcode:
		plural = ""
		if retcode > 1:
			plural = "s"
		sys.stderr.write("\nipsec verify: encountered %s error%s - see 'man ipsec_verify' for help\n"%(retcode,plural))
		sys.exit(retcode)

if __name__ == "__main__":
	main()

##  old perl code not yet ported
## 
## sub checktunnel {
##     $csource=$_[0]; $cdest=$_[1]; $ctun=$_[2]; $all="0.0.0.0/0";
## 
##     printfun "Checking $ctun from $csource to $cdest";
##     run "iptables -t nat -L POSTROUTING -n";
##     @out=grep !/(Chain POSTROUTING|target)/, @out;
##     foreach (@out) {
## 	( $target, $prot, $opt, $source, $dest ) = split(' ',$_);
## 	if(((($source eq $csource) || ($source eq $all)) && (($dest eq $cdest) || ($dest = $all))) && $target eq "ACCEPT")
## 	{ 
## 	    errchk "@out";
## 	    $reterr = 1;
## 	}
## 	else
## 	{
## 	    @err="$target from $source to $dest kills tunnel $source -> $cdest\n";
## 	    errchk "","FAILED";
## 	    $reterr = 1;
## 	}
##     }
## }
## 
## 	printfun "Checking NAT and MASQUERADEing";
## 	# This assumes KLIPS eroute information, we should add support
## 	# for NETKEY, but ip xfrm is very annoying to parse
## 	if(( -e "/proc/net/nf_conntrack" || -e "/proc/net/ip_conntrack")
## && -e "/proc/net/ipsec_eroute" )
## 	{
## 		run "iptables -t nat -L -n";
## 		if(grep /(NAT|MASQ)/, @out)
## 		{
## 		    printf "\n";
## 		    open("cat", "/proc/net/ipsec_eroute");
## 		    foreach(grep /tun0x/, <cat>)
## 		    {      
## 			@eroute=split(' ',$_);
## 			checktunnel $eroute[1], $eroute[3], $eroute[5];
## 		    }
## 		}
## 		else
## 		{
## 		    errchk "1";
## 		}
## 	}
## 	    else
## 	{ 
## 		errchk "OK";
## 	}
##     }
## }
